. _
[Server]

Server Initial Setup

1. Set Up Ubuntu

First, let's set up and prepare our fresh Ubuntu server. Install these essential tools:

To run the setup script:

      
bash
chmod +x setup-ubuntu.sh ./setup-ubuntu.sh

setup-ubuntu.sh:

      
bash
#!/usr/bin/env bash set -e GREEN="\e[32m" BLUE="\e[34m" RED="\e[31m" RESET="\e[0m" TOTAL=9 STEP=0 progress() { STEP=$((STEP + 1)) PERCENT=$((STEP * 100 / TOTAL)) FILLED=$STEP EMPTY=$((TOTAL - FILLED)) printf "\n${BLUE}[" printf "%0.s#" $(seq 1 $FILLED) printf "%0.s " $(seq 1 $EMPTY) printf "] %3d%%${RESET} %s\n" "$PERCENT" "$1" } if [ "$EUID" -ne 0 ]; then echo -e "${RED}Run with sudo${RESET}" exit 1 fi USERNAME="${SUDO_USER:-$(logname)}" HOME_DIR=$(eval echo "~$USERNAME") progress "Updating system" apt update DEBIAN_FRONTEND=noninteractive apt upgrade -y progress "Installing packages" apt install -y \ curl \ wget \ git \ unzip \ zip \ ca-certificates \ gnupg \ lsb-release \ software-properties-common \ apt-transport-https progress "Installing zsh, tmux, vim, and nvim" apt install -y zsh tmux vim neovim progress "Installing Oh My Zsh" if [ ! -d "$HOME_DIR/.oh-my-zsh" ]; then sudo -u "$USERNAME" env \ RUNZSH=no \ CHSH=no \ KEEP_ZSHRC=yes \ sh -c \ "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" fi progress "Installing Docker" install -m 0755 -d /etc/apt/keyrings if [ ! -f /etc/apt/keyrings/docker.gpg ]; then curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg fi chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ > /etc/apt/sources.list.d/docker.list apt update DEBIAN_FRONTEND=noninteractive apt install -y \ docker-ce \ docker-ce-cli \ containerd.io \ docker-buildx-plugin \ docker-compose-plugin progress "Configuring Docker" systemctl enable docker systemctl start docker usermod -aG docker "$USERNAME" mkdir -p "$HOME_DIR/.docker" chown -R "$USERNAME:$USERNAME" "$HOME_DIR/.docker" progress "Creating tmux config" cat > "$HOME_DIR/.tmux.conf" <<EOF set -g mouse on setw -g mode-keys vi set -g history-limit 100000 EOF chown "$USERNAME:$USERNAME" "$HOME_DIR/.tmux.conf" progress "Creating vim config" cat > "$HOME_DIR/.vimrc" <<EOF syntax on set number set relativenumber set tabstop=4 set shiftwidth=4 set expandtab set mouse=a EOF chown "$USERNAME:$USERNAME" "$HOME_DIR/.vimrc" progress "Changing default shell" chsh -s "$(which zsh)" "$USERNAME" echo echo -e "${GREEN}======================================" echo "Setup completed successfully!" echo "======================================${RESET}" echo echo "Please logout and log in again." echo echo "Verify these commands work:" echo "docker --version" echo "docker compose version" echo "zsh --version" echo "tmux -V" echo "nvim --version" echo

2. Create User

This script creates a user named danial, gives it sudo privileges, sets up SSH access, and disables password prompts for sudo.

To run the script:

      
bash
chmod +x setup-user.sh ./setup-user.sh

setup-user.sh:

      
bash
#!/usr/bin/env bash set -e # NOTE: Change these values! USERNAME="danial" PASSWORD="CHANGE_ME" # Optional: your public key # PUBLIC_KEY="ssh-ed25519 AAAAC3... replace_with_your_key" echo "Creating user..." if id "$USERNAME" &>/dev/null; then echo "User already exists." else useradd -m -s /bin/bash "$USERNAME" echo "$USERNAME:$PASSWORD" | chpasswd fi echo "Installing sudo..." apt update apt install -y sudo echo "Adding user to sudo group..." usermod -aG sudo "$USERNAME" echo "Adding user to docker group..." usermod -aG docker "$USERNAME" echo "Granting passwordless sudo..." cat > /etc/sudoers.d/$USERNAME <<EOF $USERNAME ALL=(ALL) NOPASSWD:ALL EOF chmod 440 /etc/sudoers.d/$USERNAME echo "Setting up SSH..." mkdir -p /home/$USERNAME/.ssh chmod 700 /home/$USERNAME/.ssh if [ -n "$PUBLIC_KEY" ]; then echo "$PUBLIC_KEY" > /home/$USERNAME/.ssh/authorized_keys chmod 600 /home/$USERNAME/.ssh/authorized_keys fi chown -R $USERNAME:$USERNAME /home/$USERNAME/.ssh echo "Disabling root SSH login..." sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config echo "Ensuring password authentication remains enabled..." grep -q "^PasswordAuthentication" /etc/ssh/sshd_config \ && sed -i 's/^PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config \ || echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config systemctl restart ssh echo echo "====================================" echo "User $USERNAME created successfully." echo "====================================" echo echo "Login with:" echo echo "ssh $USERNAME@your-server-ip" echo

If you want to connect from a Mac device, follow this:

a. On your MacBook: Create an SSH key (if you don’t already have one)

Run:

      
bash
ls ~/.ssh

If you don’t see id_ed25519 and id_ed25519.pub, generate one:

      
bash
ssh-keygen -t ed25519 -C "macbook-to-server"

Press Enter at all prompts (unless you want to change file locations).

b. Copy your public key to the server

Replace danial and SERVER_IP:

      
bash
ssh-copy-id danial@SERVER_IP

If ssh-copy-id is missing (classic Apple minimalism moment):

      
bash
cat ~/.ssh/id_ed25519.pub | ssh danial@SERVER_IP "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

c. Fix permissions on server

SSH is pretty dramatic about file permissions:

      
bash
chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys

3. Make Oh My Zsh the default for your new user

a. Ensure zsh and Oh My Zsh are installed

On the server:

      
bash
which zsh

If empty, install zsh:

      
bash
sudo apt install -y zsh

Check if Oh My Zsh exists:

      
bash
ls /home/danial/.oh-my-zsh

If missing, install it as the user:

      
bash
sudo -u danial sh -c \ "RUNZSH=no CHSH=no KEEP_ZSHRC=yes \ $(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

b. Set zsh as the default shell

Run:

      
bash
sudo chsh -s $(which zsh) danial

Check:

      
bash
getent passwd danial

You should see something like:

      
danial:x:1000:1000:...:/home/danial:/usr/bin/zsh

If it still says /bin/bash, nothing changed.


4. Create swap

a. Understand what’s happening

Linux does not use swap just because RAM is being used.

It uses swap when:

So your system is probably sitting on RAM because it thinks:

“I still have breathing room, why suffer disk I/O?”

b. Check current swap

      
bash
swapon --show free -h

c. Create a 5GB swap file

This is the modern, correct, and easy way:

      
bash
sudo fallocate -l 5G /swapfile

Secure the swap file:

      
bash
sudo chmod 600 /swapfile

Format it as swap:

      
bash
sudo mkswap /swapfile

Enable it immediately:

      
bash
sudo swapon /swapfile

d. Make it permanent

Edit /etc/fstab:

      
bash
sudo nano /etc/fstab

Add this line at the bottom:

      
fstab
/swapfile none swap sw 0 0

e. Adjust swap behavior

Linux's default swappiness is a bit “eager” with swap. For servers, you usually want less swapping.

Check the current value:

      
bash
cat /proc/sys/vm/swappiness

Temporarily set it:

      
bash
sudo sysctl vm.swappiness=10

Make it permanent:

      
bash
echo "vm.swappiness=10" | sudo tee -a /etc/sysctl.conf

Some options:

Goal swappiness
Almost never swap 10
Balanced server (recommended) 30–60
Aggressively swap 80–100

It’s helpful to restart to make sure everything was set up correctly.

      
bash
sudo shutdown -r now

5. Set up firewalls

a. Install Cockpit

      
bash
sudo apt install ufw cockpit -y

Enable Cockpit:

      
bash
sudo systemctl enable --now cockpit.socket

Now open:

https://YOUR_SERVER_IP:9090

b. Configure firewall


NOTE:
After nginx is set up, close ports 9090, 81 and 9000.
Only keep SSH, HTTP, and HTTPS ports open.


Default policy (block everything):

      
bash
sudo ufw default deny incoming sudo ufw default allow outgoing

Allow only what you need:

      
bash
sudo ufw allow 22/tcp # SSH sudo ufw allow 80/tcp # HTTP sudo ufw allow 443/tcp # HTTPS sudo ufw allow 81/tcp # NGINX sudo ufw allow 9000/tcp # Portainer sudo ufw allow 9090/tcp # Cockpit

Enable UFW:

      
bash
sudo ufw enable

Check status:

      
bash
sudo ufw status verbose

Example output:

      
bash
Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), deny (routed) New profiles: skip To Action From -- ------ ---- 22/tcp ALLOW IN Anywhere 80/tcp ALLOW IN Anywhere 443/tcp ALLOW IN Anywhere 81/tcp ALLOW IN Anywhere 9000/tcp ALLOW IN Anywhere 9090/tcp ALLOW IN Anywhere 22/tcp (v6) ALLOW IN Anywhere (v6) 80/tcp (v6) ALLOW IN Anywhere (v6) 443/tcp (v6) ALLOW IN Anywhere (v6) 81/tcp (v6) ALLOW IN Anywhere (v6) 9000/tcp (v6) ALLOW IN Anywhere (v6) 9090/tcp (v6) ALLOW IN Anywhere (v6)

c. Let Cockpit control the firewall (Optional)


NOTE:
Having both UFW and firewalld active is like asking Linux to host a cage fight. Only have one enabled.
Check with:

      
bash
sudo ufw status sudo systemctl status firewalld

Disable UFW entirely and just use firewalld:

      
bash
ufw disable

Install and enable firewalld:

      
bash
sudo apt install firewalld -y sudo systemctl enable --now firewalld

Enable ports in firewalld:

      
bash
sudo firewall-cmd --permanent --add-port=81/tcp sudo firewall-cmd --permanent --add-port=9000/tcp sudo firewall-cmd --permanent --add-port=9090/tcp sudo firewall-cmd --permanent --add-service=http sudo firewall-cmd --permanent --add-service=https sudo firewall-cmd --permanent --add-service=ssh sudo firewall-cmd --reload

NOTE:
You can find firewall configs in the web dashboard:
Cockpit → Networking → Firewall


6. Allow docker to access public internet

a. Run these to confirm current state

      
bash
systemctl status docker --no-pager -l
      
bash
docker info | sed -n '1,30p'
      
bash
sudo firewall-cmd --state
      
bash
sudo firewall-cmd --get-active-zones

b. Correct “safe Docker + firewalld” setup

      
bash
sudo firewall-cmd --permanent --zone=trusted --add-interface=docker0
      
bash
sudo firewall-cmd --permanent --add-masquerade
      
bash
sudo firewall-cmd --reload

c. Verify Docker NAT is correct

      
bash
sudo iptables -t nat -L -n -v | grep MASQUERADE

You should see Docker subnet (like 172.17.0.0/16 or 172.20.0.0/16).

d. Test real container internet

      
bash
docker run --rm alpine sh -c "apk add curl >/dev/null && curl -I https://google.com"

checklist for any issues

Run in order:

Docker status

      
bash
systemctl status docker --no-pager -l

Real error log

      
bash
journalctl -xeu docker.service --no-pager | tail -n 120

Network state

      
bash
docker network ls

NAT rules

      
bash
sudo iptables -t nat -L -n -v

Firewalld zones

      
bash
firewall-cmd --get-active-zones

7. Fix Docker not starting after apt upgrade / restart (ZONE_CONFLICT)

At least the error message tells you exactly what's wrong.

The error is:

      
ZONE_CONFLICT: 'docker0' already bound to 'trusted'

Docker wants to place docker0 into its own docker firewalld zone, but firewalld already has docker0 assigned to the trusted zone, so Docker aborts.

Check first:

      
bash
sudo firewall-cmd --get-active-zones sudo firewall-cmd --get-zone-of-interface=docker0

You'll almost certainly get:

      
trusted interfaces: docker0

Fix:

Remove docker0 from the trusted zone:

      
bash
sudo firewall-cmd --permanent --zone=trusted --remove-interface=docker0 sudo firewall-cmd --reload

Then restart Docker:

      
bash
sudo systemctl restart docker sudo systemctl status docker
0%